package org.carabiner.infinitest.depgraph;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.carabiner.infinitest.ClassFileFilter;

/**
 * 
 * @author Ben Rady
 */
public class DependencyGraph
{
  private File[] path;
  private Map<File,Long> timestampIndex;
  private Map<String,JavaClass> classNameIndex;
  private JavaClassBuilder builder;
  private ClassFileParser parser;
  private Set<JavaClass> testsToRun;
  private Set<JavaClass> changedClasses;
  private Set<JavaClass> changedParents;    
  private TestVerifier verifier;

  /**
   * 
   * @param classpath
   * @param runner Pattern used to match tests against class names
   * @throws IOException
   */
  public DependencyGraph(File[] classpath, TestVerifier testVerifier) throws IOException
  {
    verifier = testVerifier;
    path = classpath;
    classNameIndex = new HashMap<String, JavaClass>();
    timestampIndex = new HashMap<File, Long>();    

    FileManager fileManager = new FileManager();
    parser = new ClassFileParser();
    for(int i = 0; i < path.length; i++)
    {
      if (path[i].isDirectory())
        fileManager.addDirectory(path[i].getAbsolutePath());
      // FIXME add support for jars here?
    }

    builder = new JavaClassBuilder(parser, fileManager);
  }

  /**
   * Runs through the classpath looking for changed files and returns the set of
   * tests that need to be run.
   *
   * @return List
   * @throws IOException
   */
  public Set<JavaClass> update() throws IOException
  {
    // Find changed classes
    changedClasses = new HashSet<JavaClass>();
    for(int i = 0; i < path.length; i++)
    {
      changedClasses.addAll(findChangedFiles(path[i]));
    }

    // Loop through all changed classes, adding thier parents (and thier parents)
    // to another set of changed classes
    changedParents = new HashSet<JavaClass>();
    for(JavaClass jclass : changedClasses)
    {
//      callDepth = 0;
      findParents(jclass);
    }

    // combine two sets
    changedClasses.addAll(changedParents);

    // run through total set, and pick out tests to run
    testsToRun = new HashSet<JavaClass>();
    for (JavaClass jclass : changedClasses)
    {
      if (isTest(jclass))
      {
        // if it's a test, add it to the list of tests to run
        testsToRun.add(jclass);
      }
    }

    return testsToRun;
  }

//  private static int callDepth;
  private void findParents(JavaClass jclass)
  {
//    callDepth++;
//    if (callDepth > 50)
//      System.out.println("Finding parents: " + jclass.getName());
    changedParents.add(jclass);
    Set<JavaClass> parents = jclass.getParents();
    for (JavaClass parentClass : parents)
    {
      // If a parent class hasn't been checked before
      // (and it's not already marked to be run),
      // add it to the list of changed classes
      if (!changedClasses.contains(parentClass) &&
          !changedParents.contains(parentClass) && 
          !parentClass.equals(jclass))
      {
        findParents(parentClass);
      }
    }    
  }

  private JavaClass findJavaClass(String classname) throws IOException
  {
    JavaClass clazz = classNameIndex.get(classname);
    if (clazz == null)
    {
      String resourceName = getResourceName(classname);
      InputStream stream = getInputStream(resourceName);      
      clazz = parser.parse(stream);      
      addToIndex(clazz);
    }
    return clazz;
  }

  private InputStream getInputStream(String resourceName)
  {
    InputStream stream = getClass().getResourceAsStream(resourceName);
//    try
//    {
//      return new FileInputStream(path[0].getAbsolutePath() + resourceName);
//    }
//    catch (FileNotFoundException e)
//    {
//      return null;
//    }
//    for(int i = 0; i < path.length; i++)
//    {
//      if (path[i].isDirectory())
//        fileManager.addDirectory(path[i].getAbsolutePath());
//      
//    }
    if (stream == null)        
      throw new IllegalArgumentException("Class not found: " + resourceName);
    return stream;
  }

  static String getResourceName(String classname)
  {
    classname = "/" + classname.replace('.', '/') + ".class";
    return classname;
  }

  private void addToIndex(JavaClass parentClass) throws IOException
  {
    classNameIndex.put(parentClass.getName(), parentClass);
    for(String child : parentClass.getImports())
    {
      if (child.startsWith("com.") || child.startsWith("org."))
      {
        JavaClass childClass = findJavaClass(child);
        if (!childClass.equals(parentClass))
        {          
          childClass.addParent(parentClass);
        }
      }
    }
  }
  
  Collection<JavaClass> getClassesFromFile(File file) throws IOException
  {
    Collection<JavaClass> classes = builder.buildClasses(file);
    for(JavaClass clazz : classes)
    {     
      if (!classNameIndex.containsKey(clazz.getName()))
      {
        addToIndex(clazz);
      }
      else
      {
        classes.remove(clazz);
        classes.add(classNameIndex.get(clazz.getName()));
      }
    }
    return classes;
  }

  private Set<JavaClass> findChangedFiles(File file) throws IOException
  {
    Set<JavaClass> changedFiles = new HashSet<JavaClass>();
    if (file.isDirectory())
    {
      File[] children = file.listFiles(new ClassFileFilter());
      for(int i = 0; i < children.length; i++)
      {
        changedFiles.addAll(findChangedFiles(children[i]));
      }
    }
    else
    {
      if (file.getAbsolutePath().endsWith(".class"))
      {
        Long timestamp = timestampIndex.get(file);
        if (timestamp == null || file.lastModified() != timestamp)
        {
          timestampIndex.put(file, file.lastModified());
          changedFiles.addAll(getClassesFromFile(file));
        }
      }
    }
    return changedFiles;
  }

  public boolean isTest(JavaClass javaClass)
  {
    if (javaClass.isAbstract())
      return false;
    try
    {
      Class clazz = Class.forName(javaClass.getName());
      if (clazz == null)
        throw new IllegalArgumentException("cannot create class for " + javaClass.getName());
      return verifier.isATest(clazz);
    }
    catch (ClassNotFoundException e)
    {
      throw new RuntimeException(e);
    }    
  }

}
